Skip to content

docs(api): the 409 unique-constraint entry is UNIQUE_VIOLATION on the wire, not DUPLICATE_RECORD - #15750

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-15631-15632-error-docs-wire-shape
Sep 7, 2026
Merged

docs(api): the 409 unique-constraint entry is UNIQUE_VIOLATION on the wire, not DUPLICATE_RECORD#15750
os-zhuang merged 4 commits into
mainfrom
claude/issue-15631-15632-error-docs-wire-shape

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15631

Card #15632 is measured but NOT changed on this branch — its premise did not survive the measurement it asked for, and it stays open for grading. Details under "Measurement 2" below.

No longer docs-only — the second round re-points the ADR-0112 D7 guard per the #15631 ruling; see "The guard, re-pointed (ruled)" below. The first round's docs change was verified at 8888b3862, which merges origin/main (no rebase) to pick up #15762 — the deriver fix for #15749 filed from this branch. e52bb6a44 is an ancestor of HEAD (git merge-base --is-ancestor exit 0, self-certifying); the diff against origin/main is still the one file.

What changed

content/docs/api/error-catalog.mdx catalogued the 409 unique-constraint refusal under the engine's in-process spelling. Per the maintainer ruling on #14723 (2026-09-03) a unique-constraint refusal has ONE wire spelling on every route, UNIQUE_VIOLATION; DuplicateRecordError.code stays DUPLICATE_RECORD in-process only, translated at the REST door. A client branching on the catalogued constant never matched.

  • the ## Conflict Errors (409) entry is renamed to UNIQUE_VIOLATION, Cause / Fix / Retry kept, with one cross-reference sentence naming the in-process spelling and where the translation happens
  • the HTTP Status Quick Reference 409 row follows

packages/rest is the judge here, not the patient — nothing under packages/** is touched.

Per-example reconciliation

Code Status Judge Before After
UNIQUE_VIOLATION 409 packages/rest/src/error-response.ts:993 — the DuplicateRecordError arm of structuredCodeAnswer answers status: 409, code: 'UNIQUE_VIOLATION'; pinned key-for-key by packages/rest/src/rest-duplicate-record-arm.test.ts entry absent from the page; UNIQUE_VIOLATION had 0 occurrences ### UNIQUE_VIOLATION entry + quick-reference row
DUPLICATE_RECORD none on the wire packages/objectql/src/duplicate-record-error.tsreadonly code = DUPLICATE_RECORD_CODE, thrown in-process; census of code: 'DUPLICATE_RECORD' across packages/** finds no wire producer, only docblocks and that throw catalogued as a 409 wire code (entry + quick-reference row) named once, in prose, as the in-process spelling only

Live control for the "0 occurrences" reading: in the same run UNIQUE_VIOLATION matched in six other files under content/docs/ (http-protocol.mdx, error-handling.mdx, contract.mdx, error-code-ledger.mdx, drivers.mdx, releases/v17.mdx) — the zero was the page, not the instrument.

Measurement 1 — does any producer emit success / details / requestId / timestamp?

Asked of the judge first, then repo-wide.

  • structuredCodeAnswer / resolveErrorResponse: NO. Zero occurrences of all four in any emitted body in packages/rest/src/error-response.ts (the four hits in that file are prose in docblocks). Positive control: code: matches 37 times in the same file, so the grep was not blind.
  • success: YES, elsewhere. sendOk / sendError in packages/types/src/response-envelope.ts:83 and :251 write { success: true, data } and { success: false, error: { code, message, ...extra } }. 95 non-test call sites across packages/rest, packages/services/* and plugins/*.
  • details: YES, elsewhere. Carried in sendError's extra, typed as a Pick of ApiError's own optionals. Live example: packages/services/service-settings/src/settings-routes.ts:74 sends { details: { namespace: err.namespace } }.
  • requestId: DECLARED, NOT EMITTED. It is on ApiErrorSchema and accepted by sendError's extra, but no call site supplies one — the only read is response-envelope.ts:227, feeding logServerFault. Nothing puts it on a response body today.
  • timestamp: NOT EMITTED on any error body. Zero occurrences in both error-response.ts and response-envelope.ts; positive control, success matches 8 times in the latter. It exists as EnhancedApiErrorSchema.timestamp and as meta.timestamp on BaseResponseSchema / ErrorResponseSchema, both unset by any producer found.

Measurement 2 — does a real consumer read the nested shape? YES, and it stops the #15632 rewrite

This was the dispatch's stop condition, and it is met.

packages/client/src/index.ts reads BOTH envelopes' declared spots, deliberately and by name:

  • :6017asSemanticCode(errorBody?.code) ?? asSemanticCode(errorBody?.error?.code)
  • :6020Array.isArray(errorBody?.error?.details?.fields) ? errorBody.error.details.fields
  • :6035error.category = errorBody?.error?.category
  • :6043error.details = errorBody?.details ?? errorBody?.error?.details ?? errorBody

The comment above them states the reason verbatim: "The two reads below are the two LIVE envelopes' declared spots, not a fallback chain — the flat shape's retirement belongs to the envelope-convergence line (#3843)." It names both dialects explicitly: @objectstack/rest, flat and runtime dispatcher, wrapped. packages/client/src/shares-envelope-compat.test.ts pins that dual read.

So the nested envelope is not aspirational. It is declared in the spec (BaseResponseSchema, and envelopeViolations REQUIRES the nested form for a failure body: "a failure body must carry error with a string code and message — the nested form"), it has one shared writer, ~95 producer call sites, a gate ratchet (check:route-envelope), and a real SDK consumer.

This falsifies #15632's premise. The envelope published on error-handling.mdx does not match "no route's response body" — it matches the whole dispatcher and services route family. The strongest single piece of evidence is on the OTHER page in this dispatch: content/docs/api/error-catalog.mdx:536 and :587 already publish two nested examples, for GET /api/v1/meta/viewes and PUT /api/v1/meta/fieldz, and those examples are CORRECT for those routes.

Rewriting error-handling.mdx page-wide to the flat body would therefore have replaced one wrong claim with another. No bytes of that page are touched here.

What #15632 actually reports, restated from the measurement: the platform has TWO live error envelopes on different route families, and error-handling.mdx publishes one of them under the universally-quantified heading "Every error follows this structure". That is a contract-and-docs-architecture question, not a docs typo — which is why it comes back for grading rather than as a rewrite.

The guard, re-pointed (ruled)

The ACCEPT above was withdrawn because Test Core (1/6) went red in
packages/spec/src/api/error-catalog-docs.test.ts — the ADR-0112 D7 guard — on the docs change
this PR makes. The director ruled on #15631 (comment 5568179518, 2026-09-07) that the guard's
premise, not the page, is what is wrong:

  1. The catalog page catalogs wire codes. The guard compares headings against the published wire face — the error-code ledger (ERROR_CODE_LEDGER / the set scripts/check-error-status-conformance.mjs derives as reachable-on-the-wire) — in both directions: every heading is a wire code; every wire code has a heading.
  2. An enum member that the translation census marks as translated at the door (today: DUPLICATE_RECORDUNIQUE_VIOLATION) is exempt from "must have a heading" and must instead be named by the page's cross-reference sentence under its wire code's entry (the guard asserts that too, so the in-process spelling stays findable). ⛔ No bare ### DUPLICATE_RECORD heading (this card's original defect).
  3. The guard reads the translation set from the one place the conformance script already derives it — ⛔ no second hand-written list; the INVALID_REQUEST suffix accident is closed by matching headings by the same rule as everything else.
  4. Not a spec surface: a test file in packages/spec plus the docs page; skip-changeset holds. Clause-②: no.

What the diff does

scripts/check-error-status-conformance.mjs — the corpus walk, the runtime side, the doc side and
the reconciled vocabulary move into one exported deriveWireFace(), and main() becomes a
consumer of it rather than an inlining of it. The new member is wireCodes: the reconciled
vocabulary minus the codes a door translates away. That subtraction is what makes items 1 and 2 one
rule instead of two — the translated member is exempt from "must have a heading" because it is not
a wire code
, not by a special case written beside it.

The refactor is behaviour-preserving, and that is measured rather than asserted: the gate's full
stdout is byte-identical before and after (diff over the two captured runs reports no
difference), and --self-test still reports 58 cases pass.

scripts/check-error-status-conformance.d.mts (new) declares that one export for the TypeScript
consumer, per the check-declaration-mirrors convention that already covers
js-comment-mask.d.mts and check-regen-pending.d.mts. Deliberately one export: the module
exports two dozen internals for its own --self-test, and declaring those would invite the guard to
re-assemble the derivation itself — the second copy item 3 forbids. node scripts/check-declaration-mirrors.mjs — EXIT=0, and it now reads 9 declaration pairs including this
one.

packages/spec/src/api/error-catalog-docs.test.ts — the guard now imports deriveWireFace and
asserts five things:

# Assertion Ruling item
1 the derivation is non-empty and its parsed enum equals StandardErrorCode.options positive control — every other assertion is a universal over a derived collection and passes vacuously on a derivation that went blind
2 every catalog heading is a wire code 1
3 every wire code has a catalog heading 1
4 every translated code is named under its wire code's entry 2
5 the advertised count matches wireCodes.length 3

Headings are matched via the module's catalogEntries — i.e. by ENTRY_HEADING_SHAPES, the same
rule the conformance gate uses — rather than by a regex of the test's own. That is what closes the
INVALID_REQUEST accident named in item 3: the old guard's /^### \CODE`$/was anchored, and the page's two/metaentries carry a descriptive suffix, so they were never read. An unread heading is an unchecked heading. They are read now, and they pass becauseINVALID_REQUESTgenuinely *is* a wire code — a ledger code the page publishes a400` for.

content/docs/api/error-catalog.mdx — the advertised count now states what the page promises:
51 error codes reachable on the wire, not 50 enum members, with a short paragraph saying that the
catalog documents the wire face and how a translated code appears on it. The UNIQUE_VIOLATION
entry and its cross-reference sentence, accepted on 2026-09-05, are unchanged.

The numbers, derived

scope: 52 code(s) reconciled = 50 StandardErrorCode member(s) + 2 ledger code(s) a doc page
       publishes a status for (INVALID_REQUEST, UNIQUE_VIOLATION)
translated: DUPLICATE_RECORD (declared 409 at packages/objectql/src/duplicate-record-error.ts:
       class DuplicateRecordError) → UNIQUE_VIOLATION at packages/rest/src/error-response.ts:998

52 − 1 translated = 51 wire codes. The catalog's 52 read entries collapse to 51 distinct
codes
(INVALID_REQUEST has two entries). Both directions are empty sets: no heading that is not
a wire code, no wire code without a heading.

Red first, then green

The old guard is red on the merged head 0dd0dc369 with exactly the two failures the withdrawal
(comment 5550487210) quoted from the Test Core (1/6) job log — reproduced locally by checking the
old test file and the old page back out of that commit:

AssertionError: docs heading `UNIQUE_VIOLATION` is not in StandardErrorCode: expected false to be true
AssertionError: StandardErrorCode member `DUPLICATE_RECORD` has no docs entry: expected false to be true
      Tests  2 failed | 1 passed (3)

The new guard on the same tree: Test Files 1 passed (1), Tests 5 passed (5), EXIT=0.

Ablations

Both mutations the ruling names, each run on the committed tree, each proved to have reached disk
before the guard ran, and each restore proved by blob hash against HEAD plus an empty
git diff HEAD — not by a restore command's exit code.

Ablation On-disk proof Guard Which assertion fired
a bare ### \DUPLICATE_RECORD`` heading added bare-heading occurrences 0 → 1 EXIT=1, 1 failed | 4 passed every catalog heading is a wire code
the cross-reference sentence deleted DUPLICATE_RECORD occurrences 1 → 0 EXIT=1, 1 failed | 4 passed every translated code is named under its wire code's entry
content/docs/api/error-catalog.mdx:380: heading `DUPLICATE_RECORD` is not a code this platform puts
on the wire — the door translates it away, so the page must document its WIRE spelling
(UNIQUE_VIOLATION) instead and name `DUPLICATE_RECORD` in that entry's cross-reference sentence
content/docs/api/error-catalog.mdx:370: the `UNIQUE_VIOLATION` entry does not name `DUPLICATE_RECORD`.
The door translates that envelope at the boundary (packages/rest/src/error-response.ts:998), so the
in-process spelling has no entry of its own and this cross-reference is the only place a reader can
find it.

Each ablation fires one assertion and leaves the other four passing, so neither is a blanket red
that would have shown up whatever broke. Both restores: both blobs equal HEAD, git diff HEAD empty.

Gate verdicts — this round (head 3e66cce22)

node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack derives
109 commands over the 4-path change set (up from 39 when this branch was one docs file). All 109
were run, exit code captured before any pipe (cmd > log 2>&1; EXIT=$?).

95 green. Named among them: check:error-status-conformance (EXIT=0, and --self-test 58 cases pass), check:nul-bytes, check:declaration-mirrors (+ --self-test), check:cross-package-test-inputs,
check:test-source-alias, check:type-check-coverage, check:engine-double-contract,
check:page-declaration-shape, check:doc-anchors, check:doc-authoring, check:docs-single-h1,
check:scripts-symbol-anchors, check:self-test-wired, check:comment-mask-adoption.

13 NOT MEASURED — every one of them an unbuilt-tree prerequisite, none a finding. Seven exit 3
saying so in those words (check:dts-closure, check:sourcemap-no-sources-content,
check:type-check-debt, check:dual-build-cjs-loads, check:docs-transcript-drift, and the two
@objectstack/lint doc gates); six spell the same refusal with exit 1 and the text
packages/spec/dist holds no .d.ts declarations — the package is not built (check:api-surface,
check:browser-reachable-entries, check:dual-source-exports, check:entry-nameability,
check:exported-any, check:skill-examples).

Not built locally, and deliberately: all thirteen read dist/, and no file in this diff can reach
any dist/.
The only path here under packages/ is error-catalog-docs.test.ts, and
packages/spec/tsconfig.json carries "exclude": ["node_modules", "dist", "**/*.test.ts"] — a test
file is not build input. The other three paths are two root scripts/ files and a docs page. CI
builds the closure and measures all thirteen.

check:pm-dispatch-gates grades scripts/pm/dispatch-gates.mjs, which this diff does not touch; it
is derived only because the change set contains scripts/ paths.

Lint, narrowed — and the narrowing measured

  • Population: 6,275 files, computed from ESLint's own configuration (ESLint#isPathIgnored over
    git ls-files, lintable extensions only), not from an estimate.
  • Linted: 3 files — the count comes from the --format json output's own length, not from the
    command line. eslint --no-inline-config over the three lintable changed paths: 0 errors, 0
    warnings
    , EXIT=0. (The fourth changed path is .mdx.)
  • Invariance: eslint.config.mjs states and has measured that this repo "never enables type-aware
    linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, test or
    not" (its own header, around :328). With no cross-file type information in play, nothing in this
    diff can move the verdict on a file it does not touch — so the three files are the whole of what
    this diff put at risk. The repo-wide sweep is CI's.

node scripts/pm/check-governed-merges.mjs --test over the final four-path file list:
0 of 4 path(s) hit the registerNOT governed, ordinary queue landing applies. skip-changeset
holds per ruling item 4 and is already on the PR.

Gate verdicts — the first round (docs-only head 8888b3862)

All 39 commands derived by node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack were re-derived and re-run on the merged head 8888b3862, exit code captured before any pipe. The derived command list is byte-identical to the pre-merge derivation, and the change set is still the single path content/docs/api/error-catalog.mdx (merge base e52bb6a44).

39 of 39 green. The check:error-status-conformance red is gone. No command reported exit 3 / PREREQUISITE NOT MET on this run; check:skill-examples, which refused on an unbuilt packages/client-react/dist before the merge, is green here because the dependency closures were built first.

node scripts/check-error-status-conformance.mjs — EXIT=0:

  scope: 52 code(s) reconciled = 50 StandardErrorCode member(s) + 2 ledger code(s) a doc page publishes a status for (INVALID_REQUEST, UNIQUE_VIOLATION); 2246 source files scanned; 314 producer site(s) derived; 83 further ledger code(s) derived but NOT reconciled — no scanned page publishes a status for them, so there is nothing to reconcile them against.
  reconciled: 25 code(s) with a derived producer, 26 (code, status) pair(s) matched against the docs.
  unpinned: 26 documented code(s) with no derivable producer (baselined: 26).
  translated: 1 class declaration(s) a door translates away before HTTP, so the thrown code is an in-process contract and NOT a wire producer (reported, and kept out of the unpinned census — the door pins the answer under its wire spelling) —
      DUPLICATE_RECORD (declared 409 at packages/objectql/src/duplicate-record-error.ts: class DuplicateRecordError) → UNIQUE_VIOLATION at packages/rest/src/error-response.ts:992
✓ every derivable runtime status is documented, and every documented status is reachable.

--self-test — EXIT=0, 58 cases pass. pnpm check:nul-bytes — EXIT=0, scanned 7618 text file(s) ... no raw ASCII control bytes.

DUPLICATE_RECORD is no longer counted as a wire producer: the deriver now reports it under translated and names the door that performs the translation. That is exactly the distinction this branch's docs change asserts, so the gate and the page now agree instead of the gate requiring the page to publish a status for a code that never crosses HTTP.

For the record, since it is what #15749 was filed on: before the merge this same gate read ✗ DUPLICATE_RECORD: the runtime can emit HTTP 409, and no doc publishes that status for it (documented: nothing). and ✗ check:error-status-conformance — 1 finding(s). on this branch, against exit 0 on the then-current origin/main c2a336ca2 — i.e. the gate was green only because the catalog published the falsehood this PR removes. #15762 fixed the deriver rather than the docs bending to it, and the workaround weighed in the earlier revision of this section (a bare ### DUPLICATE_RECORD heading absolved through the weaker covered grade) is moot and was never taken.

Self-check greps

On content/docs/api/error-catalog.mdx after the change: DUPLICATE_RECORD occurs exactly once, in the cross-reference sentence at :361. "success": false occurs twice and "error": { twice — both are the /api/v1/meta/* examples at :536 and :587, which are correct nested bodies for those routes and are deliberately left alone (see Measurement 2).

On content/docs/protocol/kernel/error-handling.mdx: unchanged on this branch — "success": false 17, "error": { 22, all still nested, pending the #15632 grading.

The dispatch's expected self-check was "zero occurrences on both pages after the rewrite". That expectation belonged to a rewrite that the measurement stopped, so the real counts are reported instead of the expected ones.

NOT MEASURED

  • Whether other in-process-only error classes sit in the same relation to check:error-status-conformance as DuplicateRecordError does. Measured for DUPLICATE_RECORD only.
  • Whether the Console / Studio apps or the examples read the nested shape. The stop condition was already met by packages/client, so the census stopped at the SDK rather than being carried through every app.
  • Whether content/docs/references/api/error-code-ledger.mdx carries the same in-process/wire confusion. It is GENERATED and out of fences; not read for this purpose.
  • Three families that take a value from the workflow and have no value outside a CI run: check-cross-package-test-inputs.mjs --union-into, check-shard-attestation.mjs --emit, check-test-completeness.mjs. Named by the deriver as NOT MEASURED.
  • The always-runs tail (workflows with no path filter) is not in the derived list and was not run locally; CI covers it.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk

Second round (guard re-point): https://claude.ai/code/session_01TezFG8ZMrNH6n5VTNpPpdH


Generated by Claude Code

… wire (#15631)

`content/docs/api/error-catalog.mdx` catalogued `DUPLICATE_RECORD` under
`## Conflict Errors (409)` and in the HTTP Status Quick Reference. Per the
maintainer ruling on #14723 (2026-09-03) a unique-constraint refusal has ONE
wire spelling on every route, `UNIQUE_VIOLATION`; `DuplicateRecordError.code`
stays `DUPLICATE_RECORD` in-process only, translated at the REST door
(`packages/rest/src/error-response.ts`, the `DuplicateRecordError` arm of
`structuredCodeAnswer`). A client branching on the catalogued constant never
matched.

The entry is renamed to the wire code with a one-sentence cross-reference to
the in-process spelling, and the quick-reference row follows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Brings in #15762 (#15749), the error-status-conformance deriver fix that stops
counting a door-translated in-process class as a wire producer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PR #15750 (head 8888b3862, content/docs/api/error-catalog.mdx +7/−2) reviewed against the GitHub three-dot diff on the merged tree, not the report.

Per the #14723 ruling: the 409 entry is renamed UNIQUE_VIOLATION with a one-sentence cross-reference (engine throws DuplicateRecordError with in-process DUPLICATE_RECORD; the REST door translates at the boundary), and the quick-reference row follows; DUPLICATE_RECORD occurs once on the page (the cross-reference), no packages/** bytes. Fixes #15631 only — #15632 stays in the decision box with zero bytes of error-handling.mdx touched.

Measured by this seat: the deriver fix e52bb6a44 (#15762) is an ancestor of the head; on a detached probe of the branch, node scripts/check-error-status-conformance.mjs → EXIT=0, DUPLICATE_RECORD (declared 409 at packages/objectql/src/duplicate-record-error.ts: class DuplicateRecordError) → UNIQUE_VIOLATION at packages/rest/src/error-response.ts:992 under the translated: census and ✓ every derivable runtime status is documented, and every documented status is reachable. — the red the earlier head carried is gone by the deriver, not by a bare heading; git merge-tree --write-tree origin/main <branch> → EXIT=0; governed test per the earlier report NOT governed (content/docs/api/**). NOT MEASURED here: the 39-command derived family the dev re-ran on the merged head (39/39 exit 0, closures built through the lock) — the report is the record.

Flip + arm when both required jobs report success on 8888b3862. On landing #15631 closes.


Generated by Claude Code

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT WITHDRAWN — CI red is this PR's, and the fix is a contract decision, not a rework. Test Core (1/6) on 8888b3862 fails in packages/spec/src/api/error-catalog-docs.test.ts (the ADR-0112 D7 guard: every ### \CODE`heading is aStandardErrorCode` member and every member has a heading), read from the job log:

docs heading `UNIQUE_VIOLATION` is not in StandardErrorCode: expected false to be true
StandardErrorCode member `DUPLICATE_RECORD` has no docs entry: expected false to be true

Both required jobs are green (TypeScript Type Check success; Lint & Repo Gates still running at withdrawal), and Test Core (1/6) is green on main a84e1ced6 and on the other queued heads, so the red is not the base's. It is the D7 guard meeting the #14723 ruling: the enum keeps DUPLICATE_RECORD as an in-process member the door translates away, and the wire spelling UNIQUE_VIOLATION is a ledger code, not an enum member — the guard's premise ("the page and the enum can never disagree about which codes exist") no longer holds for a translated code. The existing INVALID_REQUEST ledger headings escape the guard only because they carry a suffix the regex does not match, which is an accident, not a design. The PR is HELD (draft) — nothing to re-run until the decision below is taken. The bare ### DUPLICATE_RECORD heading remains refused (#15631's ruling).


Generated by Claude Code

…ire face

The guard asserted that the error-catalog page's `### `CODE`` headings and the
`StandardErrorCode` enum agree in both directions. That premise broke twice:

  - A translated code is not on the wire. `DuplicateRecordError` declares
    `code = 'DUPLICATE_RECORD'` and the REST door translates the envelope at the
    boundary, so every route answers `UNIQUE_VIOLATION` (#14723). Demanding a
    `DUPLICATE_RECORD` heading on a page that documents the wire demands the page
    publish a code no client can receive.
  - A ledger code IS on the wire. `INVALID_REQUEST` is not an enum member, yet the
    catalog publishes two `/meta` entries for it. "Every heading is an enum member"
    should have failed on them and did not: the old regex was anchored and both
    headings carry a descriptive suffix. They passed by accident.

Per the maintainer ruling on #15631, the page catalogs the WIRE FACE and the guard
compares against that, in both directions. An enum member the translation census
marks as translated is exempt from "must have a heading" — because it is not a wire
code, not by a special case — and must instead be named by the cross-reference
sentence under its wire code's entry, which the guard now asserts.

The wire face and the translation set come from the one place that already derives
them. `check-error-status-conformance.mjs` grows `deriveWireFace()` — the corpus
walk, the runtime side, the doc side, the reconciled vocabulary and that vocabulary
minus the door's translations — and `main()` becomes a consumer of it rather than an
inlining of it. A second hand-written list of translated codes here would be exactly
the copy that file's header argues against. Matching headings by that module's
`ENTRY_HEADING_SHAPES` rather than by a regex of the test's own is the same move, and
is what closes the `INVALID_REQUEST` suffix accident.

The advertised count on the page becomes what the page now promises: 51 codes
reachable on the wire, not 50 enum members.

`scripts/check-error-status-conformance.d.mts` declares the one supported export for
the TS consumer, per the `check-declaration-mirrors` convention.

Gate output and `--self-test` are byte-identical before and after the refactor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TezFG8ZMrNH6n5VTNpPpdH
@github-actions github-actions Bot added size/m and removed size/xs labels Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ffca0df9cb09cd61410b92f3d952226b309a58c8packageMentionDocs.

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Dev report — the ADR-0112 D7 guard, re-pointed at the wire face

Resumed the interrupted patch round on this branch (previous seat cut off mid-edit by a rate limit).
No new branch, no new PR, no claim comment. Head is now 3e66cce22.

The uncommitted edit found on disk

scripts/check-error-status-conformance.mjs carried one uncommitted change: an exported
scanSources(repoRoot) — the previous seat "exporting the scan so there is one implementation of the
corpus walk". Kept in intent, widened in scope. Exporting only the walk still leaves the guard to
re-assemble the derivation on the other side (index, door arms, runtime statuses, doc parse,
vocabulary, subtraction) — six steps of a second copy, which is what ruling item 3 forbids. So the
walk went back to being private and the whole derivation became one export, deriveWireFace(), with
main() as a consumer of it rather than an inlining of it.

That refactor is behaviour-preserving and it is measured: the gate's full stdout is byte-identical
before and after (diff over the two captured runs: no output), and --self-test still reports
58 cases pass.

The diff

Path What
scripts/check-error-status-conformance.mjs deriveWireFace() — corpus walk, runtime side, doc side, reconciled vocabulary, and wireCodes = vocabulary − translated. main() consumes it.
scripts/check-error-status-conformance.d.mts new. One declared export for the TS consumer, per the check-declaration-mirrors convention.
packages/spec/src/api/error-catalog-docs.test.ts the guard, re-pointed: 5 assertions against the wire face, headings matched by the module's ENTRY_HEADING_SHAPES.
content/docs/api/error-catalog.mdx advertised count → 51 error codes reachable on the wire, plus a short paragraph on what the wire face is.

The ruling, item by item

  1. Headings vs the published wire face, both directions. wireCodes = the reconciled vocabulary
    (50 enum members + the 2 ledger codes the docs publish a status for: INVALID_REQUEST,
    UNIQUE_VIOLATION) minus the translated set. 52 − 1 = 51. The catalog's 52 read entries
    collapse to 51 distinct codes (INVALID_REQUEST has two /meta entries). Both directions are
    empty: no heading that is not a wire code; no wire code without a heading.
  2. The translated member is exempt from "must have a heading" and must be cross-referenced. It is
    exempt because it is not a wire code — the subtraction does it, not a special case beside it.
    The fifth assertion then demands the cross-reference, so the exemption cannot quietly become
    "DUPLICATE_RECORD may vanish from the page". No bare ### DUPLICATE_RECORD heading, and the
    ablation below proves adding one goes red.
  3. One derivation, no second list. The translation set is deriveWireFace().translated, read from
    the door's own source by the same code the conformance gate uses. The INVALID_REQUEST suffix
    accident is closed by matching headings through that module's catalogEntries instead of a regex
    of the test's own: the old /^### \CODE`$/was anchored, so the page's two suffixed/metaentries were never read at all. They are read now, and they pass —INVALID_REQUEST` genuinely is a
    wire code.
  4. skip-changeset holds, Clause-②: no. Label already on the PR; Check Changeset is skipped
    on this head. check-governed-merges --test over the final four-path list: 0 of 4 path(s) hit the register — NOT governed.

Red first, then green

The old guard, re-checked out onto the merged head 0dd0dc369 together with that commit's page,
reproduces exactly the two failures the withdrawal (comment 5550487210) read from the Test Core (1/6) job log:

AssertionError: docs heading `UNIQUE_VIOLATION` is not in StandardErrorCode: expected false to be true
AssertionError: StandardErrorCode member `DUPLICATE_RECORD` has no docs entry: expected false to be true
      Tests  2 failed | 1 passed (3)

New guard, same tree: Test Files 1 passed (1), Tests 5 passed (5), EXIT=0.

Ablations

Run on the committed tree. Each mutation was proved to have reached disk by counting the exact token
it moves — before and after — and each restore was proved by blob hash against HEAD plus an empty
git diff HEAD, never by a restore command's exit code. The script carried a
trap ... EXIT INT TERM with absolute paths throughout.

Ablation On-disk proof Guard Assertion that fired
bare ### \DUPLICATE_RECORD`` heading added bare-heading count 0 → 1 EXIT=1, 1 failed | 4 passed every catalog heading is a wire code
cross-reference sentence deleted DUPLICATE_RECORD count 1 → 0 EXIT=1, 1 failed | 4 passed every translated code is named under its wire code's entry

Each fires one assertion and leaves the other four green, so neither is a blanket red that any
breakage would have produced. Both restores reported both blobs equal HEAD, git diff HEAD empty, and
the final git status --short was empty.

The guard's failure text carries the remedy, not just the disagreement — e.g.

content/docs/api/error-catalog.mdx:380: heading `DUPLICATE_RECORD` is not a code this platform puts
on the wire — the door translates it away, so the page must document its WIRE spelling
(UNIQUE_VIOLATION) instead and name `DUPLICATE_RECORD` in that entry's cross-reference sentence

A fifth assertion exists purely as a positive control: the derivation must be non-empty and its
regex-parsed enum must equal StandardErrorCode.options. Every other assertion is a universal over a
derived collection and would pass vacuously on a derivation that had gone blind.

Gates

dispatch-gates --changed --commands --repo objectstack-ai/objectstack derives 109 commands over
the 4-path change set (39 when this branch was one docs file). All 109 run, exit code captured before
any pipe.

  • 95 green, including check:error-status-conformance (+ --self-test, 58 cases pass),
    check:nul-bytes, check:declaration-mirrors (+ --self-test; it now reads 9 pairs including the
    new one), check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage,
    check:engine-double-contract, check:page-declaration-shape, check:doc-anchors,
    check:doc-authoring.
  • 13 NOT MEASURED, every one an unbuilt-tree prerequisite and none a finding: 7 exit 3 saying so
    in those words, 6 spelling the same refusal with exit 1 and packages/spec/dist holds no .d.ts declarations — the package is not built. All thirteen read dist/, and no path in this diff can
    reach any dist/
    — the only packages/ path is a .test.ts, and packages/spec/tsconfig.json
    excludes **/*.test.ts from the build. CI builds the closure and measures all thirteen.
  • Two gates first hit my 240 s per-command budget rather than a finding (exit 124, NOT MEASURED).
    check:query-options-erasure re-ran to EXIT=0 with a real budget. check:pm-dispatch-gates is a
    ~1000-case self-test of scripts/pm/dispatch-gates.mjs — a script this diff does not touch, derived
    only because the change set contains scripts/ paths.

Lint, narrowed — and the narrowing measured, not asserted: population 6,275 files, computed
from ESLint's own configuration (ESLint#isPathIgnored over git ls-files), not estimated; 3 files
linted
, that count taken from the --format json output's own length; 0 errors, 0 warnings,
EXIT=0. Invariance: eslint.config.mjs states and has measured that this repo "never enables
type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file,
test or not", so nothing in this diff can move the verdict on a file it does not touch. The repo-wide
sweep is CI's.

CI on 3e66cce22

All 37 check-runs completed with zero non-success conclusions, and all six required contexts are
success:

Required context Conclusion
Lint & Repo Gates success
TypeScript Type Check success
Test Core success
Dogfood Regression Gate success
Build Core success
Temporal Conformance (live PG + MySQL) success

Test Core is the family that carried the withdrawal's red — Test Core (1/6), the shard that ran
error-catalog-docs.test.ts, is green on this head. Check Changeset is skipped, which is
skip-changeset doing its job per ruling item 4.

Flipping ready re-triggered CI on the same head, as it always does; auto-merge holds the PR until
that generation is green, which is the queue's own re-verification and not something this seat waits
on.

Queue action

Non-governed (0 of 4 path(s) hit the register), green, and accepted-pending — so the ordinary
AGENTS.md §7 path applies: ready first, arm second. Both confirmed by timeline events, which is
the effect reading rather than the response:

2026-09-07T11:45:57Z  ready_for_review
2026-09-07T11:46:12Z  auto_merge_enabled

Arming stores method: MERGE no matter what is requested; that is immaterial here because main's
merge-queue rule carries merge_method: SQUASH and the queue performs the merge — one squash commit
per PR. ⛔ Not re-armed and will not be: the auto_merge field is unstable within the minute after
arming, and a re-arm kicks the entry out of the queue and re-sorts it.

Out of scope, filed

Not measured

  • The 13 dist/-reading gates listed above (unbuilt tree; no path in this diff can reach a dist/).
  • The repo-wide pnpm lint sweep; the narrowing over 3 of 6,275 files is measured and stated above.
  • check:pm-dispatch-gates — a ~1,400-case self-test of a script this diff does not touch, derived
    only because the change set contains scripts/ paths. Still running at report time (1,400+ cases and counting under box contention); it is covered by Lint & Repo Gates, which is green on this head.

Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit eda6a60 Sep 7, 2026
39 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-15631-15632-error-docs-wire-shape branch September 7, 2026 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] content/docs/api/error-catalog.mdx lists DUPLICATE_RECORD under Conflict Errors (409) with no note that the wire spelling is UNIQUE_VIOLATION

2 participants